package top.codedance.iotp.client.plugins.async.opc;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import top.codedance.iotp.client.ClientApplication;
import top.codedance.iotp.common.executer.AsyncAbstractExecuter;
import top.codedance.iotp.common.entity.CommonEntity;
import top.codedance.iotp.common.util.SystemUtil;
import com.google.common.collect.ImmutableList;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.ULong;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import org.jinterop.dcom.core.JIUnsignedInteger;
import org.jinterop.dcom.core.JIUnsignedShort;

import java.io.File;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;

import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;

public class UaOpcService extends Service {
    private Logger logger = ClientApplication.getLogger(UaOpcService.class);

    private final String applicationName = "CodebanceOpcUaClient";
    private final String applicationUri = "urn:codebance:opcua:client";

    private OpcUaClient client = null;
    private boolean status = false;
    private Set<String> subscribeTags = new LinkedHashSet<>();
    private List<MonitoredItemCreateRequest> requests = new ArrayList<>();

    public UaOpcService(AsyncAbstractExecuter executer, OpcServiceParam param) {
        super(executer, param);
    }

    /**
     * 创建联接
     */
    @Override
    public synchronized void createConnect() throws Exception {
        Path securityTempDir = new File(SystemUtil.getRuntimeDirPath(ClientApplication.class)).toPath();
        final KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir, param.getStrength(), param.getAlgorithm(), param.getValidity(), applicationName, applicationUri);

        // 创建OPC UA客户端
        client = OpcUaClient.create(
                param.getEndpointUrl(),
                (endpoints) -> {
                    Stream<EndpointDescription> stream = endpoints.stream();
                    Stream<EndpointDescription> endpointDescriptionStream = stream.filter(e -> e.getSecurityPolicyUri().equals(param.getSecurityPolicy().getUri()));
                    return endpointDescriptionStream.findFirst();
                },
                configBuilder -> {
                    configBuilder.setApplicationName(LocalizedText.english(applicationName)).setApplicationUri(applicationUri);
                    if(param.getSecurityPolicy() != SecurityPolicy.None) {
                        configBuilder.setCertificate(loader.getClientCertificate())
                                .setCertificateChain(loader.getClientCertificateChain())
                                .setKeyPair(loader.getClientKeyPair());
                    }
                    if(!param.getUser().isEmpty() && !param.getPassword().isEmpty()) {
                        configBuilder.setIdentityProvider(new UsernameProvider(param.getUser(), param.getPassword()));
                    }else {
                        configBuilder.setIdentityProvider(new AnonymousProvider());
                    }
                    return configBuilder.setRequestTimeout(uint(5000)).build();
                }
        );
        client.connect().whenComplete((c, ex) -> {
            if (ex == null) {
                UaOpcService.this.status = true;
            }
        });
    }

    public void subscribe(){
        Set<String> tags = OpcExecuterImpl.tasks.keySet();
        List<NodeId> nodeIds = new ArrayList<>();
        for (String tag : tags){
            if(!subscribeTags.contains(tag)) {
                nodeIds.add(new NodeId(2, tag));
            }
        }
        try {
            subscribe(nodeIds);
        }catch (Exception e){
            logger.debug(e.getMessage());
        }
    }

    @Override
    public void close() {
        client.disconnect();
    }

    /**
     * @MethodName: subscribe
     * @Description: 订阅节点变量
     * @throws Exception
     */
    public void subscribe(List<NodeId> nodes) throws Exception {
        if (client == null) {
            return;
        }

        // 查询订阅对象，没有则创建
        UaSubscription subscription = null;
        ImmutableList<UaSubscription> subscriptionList = client.getSubscriptionManager().getSubscriptions();
        if (CollectionUtils.isEmpty(subscriptionList)) {
            subscription = client.getSubscriptionManager().createSubscription(1000.0).get();
        } else {
            subscription = subscriptionList.get(0);
        }

        if (!CollectionUtils.isEmpty(nodes)) {
            for (NodeId node : nodes) {
                if(!subscribeTags.contains(node.getIdentifier().toString())) {
                    // 创建监控的参数
                    MonitoringParameters parameters = new MonitoringParameters(subscription.nextClientHandle(), 1000.0, // sampling
                            // interval
                            null, // filter, null means use default
                            uint(10), // queue size
                            true // discard oldest
                    );
                    // 创建订阅的变量， 创建监控项请 求
                    MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(
                            new ReadValueId(node, AttributeId.Value.uid(), null, null), MonitoringMode.Reporting, parameters);
                    requests.add(request);
                    subscribeTags.add(node.getIdentifier().toString());
                }
            }
        }

        if(requests.size() > 0) {
            // 创建监控项，并且注册变量值改变时候的回调函数
            subscription.createMonitoredItems(TimestampsToReturn.Both, requests, (item, id) -> {
                item.setValueConsumer((i, v) -> {
                    String identifier = i.getReadValueId().getNodeId().getIdentifier().toString();
                    Object value = parseValue(v.getValue());
                    receipt(identifier, value);
                });
            }).get();
        }
    }

    /**
     * 单个写入
     * @param nodeId
     * @param value
     * @throws ExecutionException
     * @throws InterruptedException
     */
    private void write(NodeId nodeId, Object value) throws ExecutionException, InterruptedException {
        if(value instanceof BigDecimal){
            value = ((BigDecimal) value).doubleValue();
        }
        Variant var = new Variant(value);
        DataValue dv = new DataValue(var, null, null, null);
        CompletableFuture<StatusCode> statusCodeCompletableFuture = client.writeValue(nodeId, dv);
        StatusCode statusCode = statusCodeCompletableFuture.get();
        logger.debug("Write status: " + statusCode.isGood());
    }

    /**
     * 批量写入
     * @param writeArray
     */
    private void writes(List<JSONObject> writeArray){
        try {
            List<NodeId> items = new ArrayList<>();
            List<String> tags = new ArrayList<>();
            Map<String, Object> exts = new HashMap<>();
            Map<String, Object> values = new HashMap<>();
            List<NodeId> nodeIds = new ArrayList<>();
            List<DataValue> dvs = new ArrayList<>();
            for (JSONObject jo : writeArray) {
                items.add(new NodeId(jo.getInteger("ns"), jo.getString("tag")));
                tags.add(jo.getString("tag"));
                values.put(jo.getString("tag"), jo.get("value"));
                nodeIds.add(new NodeId(jo.getInteger("ns"), jo.getString("tag")));
            }
            List<DataValue> dataValues = client.readValues(0.0, TimestampsToReturn.Both, items).get();
            for (int i = 0;i < dataValues.size();i++) {
                DataValue dv = dataValues.get(i);
                String s = dv.getValue().getDataType().get().getIdentifier().toString();
                String t = tags.get(i);
                //类型定义在：org.eclipse.milo.opcua.stack.core.Identifiers
                if(s.equals("1")){
                    dvs.add(new DataValue(new Variant(Boolean.valueOf(values.get(t).toString())), null, null, null));
                }else if(s.equals("3")){
                    dvs.add(new DataValue(new Variant(Byte.valueOf(values.get(t).toString())), null, null, null));
                }else if(s.equals("4")){
                    dvs.add(new DataValue(new Variant(Short.valueOf(values.get(t).toString())), null, null, null));
                }else if(s.equals("6") || s.equals("27")){
                    dvs.add(new DataValue(new Variant(Integer.valueOf(values.get(t).toString())), null, null, null));
                }else if(s.equals("8") || s.equals("26")){
                    dvs.add(new DataValue(new Variant(Long.valueOf(values.get(t).toString())), null, null, null));
                }else if(s.equals("10")){
                    dvs.add(new DataValue(new Variant(Float.valueOf(values.get(t).toString())), null, null, null));
                }else if(s.equals("11")){
                    dvs.add(new DataValue(new Variant(Double.valueOf(values.get(t).toString())), null, null, null));
                }else if(s.equals("12")){
                    dvs.add(new DataValue(new Variant(values.get(t).toString()), null, null, null));
                }else if(s.equals("13")){
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    Date d = sdf.parse(values.get(t).toString());
                    DateTime dt = new DateTime(d);
                    dvs.add(new DataValue(new Variant(dt), null, null, null));
                }else if(s.equals("292")){
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    Date d = sdf.parse(values.get(t).toString());
                    dvs.add(new DataValue(new Variant(d.getTime()), null, null, null));
                }else if(s.equals("293")){
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    dvs.add(new DataValue(new Variant(sdf.parse(values.get(t).toString())), null, null, null));
                }else if(s.equals("294")){
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    Date d = sdf.parse(values.get(t).toString());
                    DateTime dt = new DateTime(d);
                    dvs.add(new DataValue(new Variant(dt.getUtcTime()), null, null, null));
                }else{
                    dvs.add(new DataValue(new Variant(values.get(t)), null, null, null));
                }

            }
            client.writeValues(nodeIds, dvs).get();
        }catch (Exception ex){
            logger.debug(ex.getMessage());
        }
    }

    @Override
    public void receipt(String tag, Object body) {
        synchronized (OpcExecuterImpl.class) {
            JSONObject jo = new JSONObject();
            jo.put(tag, body);
            Set<Map.Entry<String, Set<String>>> entries = OpcExecuterImpl.tasks.entrySet();
            for (Map.Entry<String, Set<String>> entry : entries){
                if(entry.getKey().equals(tag)){
                    CommonEntity ce = genCommonEntity();
                    ce.setBody(jo.toJSONString().getBytes(StandardCharsets.UTF_8));
                    Set<String> value = entry.getValue();
                    for (String cid : value){
                        executer.send(ce.build(), cid);
                        logger.debug("Send data to " + cid);
                    }
                    break;
                }
            }
        }
    }

    @Override
    public void send(String tag, int ns, Object value) {
        try {
            NodeId nodeId = new NodeId(ns, tag);
            write(nodeId, value);
        }catch (Exception e){
            logger.debug(e.getMessage());
        }
    }

    @Override
    public void send(JSONArray writeArray){
        writes(writeArray);
    }

    @Override
    public boolean isRunning() {
        return status;
    }

    /**
     *解析值
     * @param vO
     * @return
     */
    private Object parseValue(Variant vO){
        Object ev = vO.getValue();
        if (ev instanceof Boolean) {
            return Boolean.valueOf(ev.toString());
        } else if (ev instanceof Long || ev instanceof Integer || ev instanceof Short || ev instanceof Byte) {
            return Long.valueOf(ev.toString());
        } else if (ev instanceof Double || ev instanceof Float) {
            return Double.valueOf(ev.toString());
        }else if (ev instanceof JIUnsignedShort) {
            JIUnsignedShort vl = (JIUnsignedShort) ev;
            return Long.valueOf(vl.getValue().toString());
        }else if (ev instanceof UShort) {
            UShort vl = (UShort) ev;
            return vl.longValue();
        }else if (ev instanceof JIUnsignedInteger) {
            JIUnsignedInteger vi = (JIUnsignedInteger) ev;
            return Integer.valueOf(vi.getValue().toString());
        }else if (ev instanceof UInteger) {
            UInteger uev = (UInteger) ev;
            return uev.intValue();
        }else if (ev instanceof ULong) {
            ULong uvl = (ULong) ev;
            return uvl.longValue();
        }else if (ev instanceof Date) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return sdf.format(ev);
        }else if (ev instanceof DateTime) {
            DateTime dt = (DateTime)ev;
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            return sdf.format(dt.getJavaDate());
        } else {
            return String.valueOf(ev);
        }
    }

    /**
     * 获取值
     * @param id
     * @param statusCode
     * @param value
     * @param jsonObject
     * @param ext
     */
    private void getValue(String id, StatusCode statusCode, Object value, JSONObject jsonObject, Object ext){
        jsonObject.put("id", id);
        if(statusCode.isGood()){
            jsonObject.put("type", value.getClass().getName());
            jsonObject.put("value", value);
            jsonObject.put("good", true);
        }else{
            jsonObject.put("type", value.getClass().getName());
            jsonObject.put("value", null);
            jsonObject.put("good", false);
            logger.debug(id + ": " + statusCode.toString());
        }
        jsonObject.put("ext", ext);
    }
}
